package com.craftworks.driver.bluetop;

import com.craftworks.mpeg2.MPEGConstants;
import com.craftworks.mpeg2.MPEGException;
import com.craftworks.mpeg2.transmission.Channel;
import com.craftworks.mpeg2.transmission.IODevice;
import com.craftworks.mpeg2.transmission.OutputChannel;
import com.craftworks.util.StringUtils;
import java.io.IOException;
import java.util.Properties;

final class BSTC12V5 implements IODevice
{
    static final int OUTPUT_MODE_ASI_SPI = 0;
    static final int OUTPUT_MODE_ASI     = 1;
    static final int OUTPUT_MODE_SPI     = 2;
    private final int device;
    private final Properties properties;
    private Channel channel;
    private boolean closed;

    BSTC12V5(int id) throws IOException
    {
        if (!BSTC12V5Adapter.open(id))
            throw new IOException("cannot open device(" + id + ")");

        device = id;
        properties = new Properties();
        channel = null;
        closed = false;

        // setup default properties
        properties.setProperty("bitrate", "38000000");
        properties.setProperty("frame_size", "188");
        properties.setProperty("output_mode", "asi & spi");
        properties.setProperty("enable_asi_burst_mode", "no");
        properties.setProperty("enable_input_reference_clock", "no");
        properties.setProperty("enable_output_reference_clock", "no");
    }

    private void ensureOpen() throws IOException
    {
        if (closed)
            throw new IOException("device closed.");
    }

    @Override
    public Properties getProperties() throws IOException
    {
        ensureOpen();
        return new Properties(properties);
    }

    @Override
    public String getProperty(String key) throws IOException
    {
        ensureOpen();
        return properties.getProperty(key);
    }

    @Override
    public String setProperty(String key, String value) throws IOException
    {
        ensureOpen();
        switch (key)
        {
            case "bitrate":
                if (!StringUtils.isNumber(value))
                    throw new IOException("value must be a number.");
                if (Long.decode(value) < 10000 || Long.decode(value) > 100000000)
                    throw new IOException("bitrate should be great than 10kbps and less than 100mbps.");
                break;
            case "frame_size":
                if (!StringUtils.isNumber(value))
                    throw new IOException("value must be a number.");
                if (Integer.decode(value) != 188 || Integer.decode(value) != 204)
                    throw new IOException("frame size must be either 188 or 204.");
                break;
            case "output_mode":
                if (!value.equals("asi") && !value.equals("spi") && !value.equals("asi & spi"))
                    throw new IOException("output mode can be either \"asi\" or \"spi\" or \"asi & spi\"");
                break;
            case "enable_asi_burst_mode":
            case "enable_input_reference_clock":
            case "enable_output_reference_clock":
                if (!value.equals("yes") && !value.equals("no"))
                    throw new IOException("enable value can be either \"yes\" or \"no\"");
                break;
        }
        return (String) properties.setProperty(key, value);
    }

    @Override
    public Channel openChannel(Channel.Type type) throws MPEGException, IOException
    {
        ensureOpen();

        if (channel != null)
            throw new IOException("there is a open channel.");

        if (type == Channel.Type.OUT)
        {
            channel = new BSTChannel(bitrate(), frameSize(), outputMode(),
                                     enabledASIBurstMode(),
                                     enabledInputReferenceClock(),
                                     enabledOutputReferenceClock());
        }

        return channel;
    }

    @Override
    public void closeChannel(Channel channel) throws IOException
    {
        if (!holds(channel))
            throw new IllegalArgumentException("subject channel is not acquired from this device");

        this.channel = null;
    }

    @Override
    public boolean holds(Channel channel) throws IOException
    {
        ensureOpen();
        return (this.channel == channel);
    }

    @Override
    public String describe(Channel channel) throws IOException
    {
        return holds(channel)
               ? String.format("an %s channel for bstc-12 card", channel.type().name().toLowerCase())
               : "an unknown channel";
    }

    @Override
    public void close() throws Exception
    {
        if (closed)
            return;

        closed = true;
        if (channel != null)
            channel.close();
        BSTC12V5Adapter.close(device);
        System.out.println("device closed.");
    }

    private long bitrate()
    {
        return Long.parseLong(properties.getProperty("bitrate", "38000000"));
    }

    private int frameSize()
    {
        return Integer.parseInt(properties.getProperty("frame_size", "188"));
    }

    private int outputMode()
    {
        String mode = properties.getProperty("output_mode", "asi & spi");
        int outputMode = OUTPUT_MODE_ASI_SPI;
        if (mode.equals("asi"))
            outputMode = OUTPUT_MODE_ASI;
        if (mode.equals("spi"))
            outputMode = OUTPUT_MODE_SPI;
        return outputMode;
    }

    private boolean enabledASIBurstMode()
    {
        String mode = properties.getProperty("enable_asi_burst_mode", "no");
        return mode.equals("yes");
    }

    private boolean enabledInputReferenceClock()
    {
        String mode = properties.getProperty("enable_input_reference_clock", "no");
        return mode.equals("yes");
    }

    private boolean enabledOutputReferenceClock()
    {
        String mode = properties.getProperty("enable_output_reference_clock", "no");
        return mode.equals("yes");
    }

    final class BSTChannel implements OutputChannel
    {
        private long bitrate;
        private boolean closed;

        public BSTChannel(long bitrate, int frameSize, int outputMode,
                          boolean enableASIBurstMode,
                          boolean enableInputReferenceClock,
                          boolean enableOutputReferenceClock) throws IOException
        {
            if (!BSTC12V5Adapter.setup(device, bitrate, frameSize, outputMode,
                                       enableASIBurstMode,
                                       enableInputReferenceClock,
                                       enableOutputReferenceClock))
                throw new IOException("cannot setup transmission parameters.");
            this.bitrate = bitrate;
            this.closed = false;
        }

        private void ensureOpen() throws IOException
        {
            if (closed)
                throw new IOException("channel closed.");
        }

        @Override
        public long getMaxOutputBitrate() throws IOException
        {
            ensureOpen();
            return -1;
        }

        @Override
        public long getOutputBitrate() throws IOException
        {
            ensureOpen();
            return bitrate;
        }

        @Override
        public void setOutputBitrate(long bitrate) throws MPEGException, IOException
        {
            ensureOpen();
        }

        @Override
        public int writePacket(byte[] buffer, int offset) throws MPEGException, IOException
        {
            ensureOpen();
            int sent = BSTC12V5Adapter.send(device, buffer, offset, MPEGConstants.TS_PACKET_SIZE);
            if (sent < 0)
                throw new IOException("no transmit operation performed.");
            return sent;
        }

        @Override
        public long writePackets(byte[] buffer, int offset, int count) throws MPEGException, IOException
        {
            ensureOpen();
            int sent = BSTC12V5Adapter.send(device, buffer, offset, MPEGConstants.TS_PACKET_SIZE * count);
            if (sent < 0)
                throw new IOException("no transmit operation performed.");
            return sent;
        }

        @Override
        public void flush() throws MPEGException, IOException
        {
            ensureOpen();
            int sent = BSTC12V5Adapter.flush(device);
            if (sent < 0)
                throw new IOException("no flush operation performed.");
        }

        @Override
        public Type type()
        {
            return Channel.Type.OUT;
        }

        @Override
        public void close() throws Exception
        {
            if (closed)
                return;

            closed = true;
            BSTC12V5Adapter.finish(device);
            closeChannel(this);
        }
    }
}
